Passed
Pull Request — master (#160)
by
unknown
01:53
created

Frame.ts ➔ getDataLength   A

Complexity

Conditions 2

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 2
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as GenericFrames from './frames/generic'
8
import { Frames } from './frames/frames'
9
import * as ID3Util from './ID3Util'
10
import { isKeyOf } from "./util"
11
12
type HeaderInfo = {
13
    identifier: string
14
    headerSize: number
15
    bodySize: number
16
    flags: Flags
17
}
18
19
export class Frame {
20
    identifier: string
21
    private value: unknown
22
    flags: Flags
23
24
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
25
        this.identifier = identifier
26
        this.value = value
27
        this.flags = flags
28
    }
29
30
    static createFromBuffer = createFromBuffer
31
32
    getValue() {
33
        return this.value
34
    }
35
}
36
37
type FrameData = {
38
    header: HeaderInfo
39
    body: Buffer
40
}
41
42
function getFrameDataFromFrameBuffer(
43
    frameBuffer: Buffer,
44
    version: number
45
): FrameData | null {
46
    const headerSize = getHeaderSize(version)
47
    // Specification requirement
48
    if (frameBuffer.length < headerSize + 1) {
49
        return null
50
    }
51
    const headerBuffer = frameBuffer.subarray(0, headerSize)
52
    const header: HeaderInfo = {
53
        headerSize,
54
        ...FrameHeader.createFromBuffer(headerBuffer, version)
55
    }
56
    if (header.flags.encryption) {
57
        return null
58
    }
59
60
    const body = decompressBody(
61
        header.flags,
62
        getDataLength(header, frameBuffer),
63
        getBody(header, frameBuffer)
64
    )
65
    if (!body) {
66
        return null
67
    }
68
    return { header, body }
69
}
70
71
function createFromBuffer(
72
    frameBuffer: Buffer,
73
    version: number
74
): Frame | null {
75
    const frameData = getFrameDataFromFrameBuffer(frameBuffer, version)
76
    if (!frameData) {
77
        return null
78
    }
79
    const { header, body } = frameData
80
    const value = makeFrameValue(header.identifier, body, version)
81
    if (!value) {
82
        return null
83
    }
84
    return new Frame(header.identifier, value, header.flags)
85
}
86
87
function makeFrameValue(identifier:string, body: Buffer, version: number) {
88
    try {
89
        if (isKeyOf(identifier, Frames)) {
90
            return Frames[identifier].read(body, version)
91
        }
92
        if (identifier.startsWith('T')) {
93
            return GenericFrames.GENERIC_TEXT.read(body)
94
        }
95
        if (identifier.startsWith('W')) {
96
            return GenericFrames.GENERIC_URL.read(body)
97
        }
98
    } catch(error) {
99
        // On read ignore frames with errors
100
        return null
101
    }
102
    return null
103
}
104
105
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
106
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
107
    const bodyStart = headerSize + bodyOffset
108
    const bodyEnd = bodyStart + bodySize - bodyOffset
109
    const body = buffer.subarray(bodyStart, bodyEnd)
110
    if (flags.unsynchronisation) {
111
        // This method should stay in ID3Util for now because it's also used
112
        // in the Tag's header which we don't have a class for.
113
        return ID3Util.processUnsynchronisedBuffer(body)
114
    }
115
    return body
116
}
117
118
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
119
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
120
}
121
122
function decompressBody(
123
    {compression}: Flags,
124
    dataLength: number,
125
    body: Buffer
126
) {
127
    return compression ? decompressBuffer(body, dataLength) : body
128
}
129
130
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
131
    if (buffer.length < 5) {
132
        return null
133
    }
134
135
    // ID3 spec defines that compression is stored in ZLIB format,
136
    // but doesn't specify if header is present or not.
137
    // ZLIB has a 2-byte header.
138
    // 1. try if header + body decompression
139
    // 2. else try if header is not stored (assume that all content is deflated "body")
140
    // 3. else try if inflation works if the header is omitted (implementation dependent)
141
    const tryDecompress = () => {
142
        try {
143
            return zlib.inflateSync(buffer)
144
        } catch (error) {
145
            try {
146
                return zlib.inflateRawSync(buffer)
147
            } catch (error) {
148
                try {
149
                    return zlib.inflateRawSync(buffer.subarray(2))
150
                } catch (error) {
151
                    return null
152
                }
153
            }
154
        }
155
    }
156
    const decompressed = tryDecompress()
157
    if (decompressed && decompressed.length === expectedDecompressedLength) {
158
        return decompressed
159
    }
160
    return null
161
}
162